package org.skydiveapplibrary.util;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import org.skydiveapplibrary.R;
import org.skydiveapplibrary.data.Aircraft;
import org.skydiveapplibrary.data.AltitudeUnit;
import org.skydiveapplibrary.data.Database;
import org.skydiveapplibrary.data.Location;
import org.skydiveapplibrary.data.LogEntryImage;
import org.skydiveapplibrary.data.LogEntryImageType;
import org.skydiveapplibrary.data.Rig;
import org.skydiveapplibrary.data.RigComponent;
import org.skydiveapplibrary.data.RigReminder;
import org.skydiveapplibrary.data.Signature;
import org.skydiveapplibrary.data.SkydiveType;
import org.skydiveapplibrary.repository.AircraftRepository;
import org.skydiveapplibrary.repository.DatabaseCursor;
import org.skydiveapplibrary.repository.LocationRepository;
import org.skydiveapplibrary.repository.LogEntryRepository;
import org.skydiveapplibrary.repository.RigRepository;
import org.skydiveapplibrary.repository.SettingsRepository;
import org.skydiveapplibrary.repository.SignatureRepository;
import org.skydiveapplibrary.repository.SkydiveTypeRepository;
import org.skydiveapplibrary.ui.FormatUtility;
import org.xmlpull.v1.XmlSerializer;

import android.content.Context;
import android.database.Cursor;
import android.util.Base64;
import android.util.Xml;

public class DatabaseExportUtil
{
    private static final String NEW_LINE = "\n";
    private static final String INDENT = "  ";
    private static final String NAMESPACE = "http://www.skydiving-logbook.org/logbook";

    // context
    private Context _context;
    private FileManager _fileManager;

    // maps
    private Map<String, SkydiveType> skydiveTypeMap = new HashMap<String, SkydiveType>();
    private Map<String, Location> locationMap = new HashMap<String, Location>();
    private Map<String, Aircraft> aircraftMap = new HashMap<String, Aircraft>();
    private Map<String, Rig> rigMap = new HashMap<String, Rig>();
    
    // exported image files
    private List<LogEntryImage> exportedImageFiles = new ArrayList<LogEntryImage>();

    // date format
    private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    private DateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    public DatabaseExportUtil(Context context, FileManager fileManager)
    {
        _context = context;
        _fileManager = fileManager;
    }

    public void exportDatabase(Writer xmlWriter, Writer csvWriter) throws IOException
    {
        initializeData();
        XmlSerializer xmlSerializer = initializeXml(xmlWriter);
        CsvSerializer csvSerializer = initializeCsv(csvWriter);

        // set timezone for date/time format
        dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));

        // start
        startTagWithAttribute(xmlSerializer, "skydiving_logbook", "version", "2", 0);

        // add logbook history
        addLogbookHistory(xmlSerializer);
        // log entries
        addLogEntries(xmlSerializer, csvSerializer);
        // signatures
        addSignatures(xmlSerializer);
        // skydive types
        addSkydiveTypes(xmlSerializer);
        // locations
        addLocations(xmlSerializer);
        // aircrafts
        addAircrafts(xmlSerializer);
        // rigs
        addRigs(xmlSerializer);

        endTag(xmlSerializer, "skydiving_logbook", 0);
        xmlSerializer.endDocument();
    }
    
    public List<LogEntryImage> getExportedImageFiles()
    {
        return exportedImageFiles;
    }

    private void initializeData()
    {
        // clear all maps
        skydiveTypeMap.clear();
        aircraftMap.clear();
        locationMap.clear();
        rigMap.clear();
        
        // clear image list
        exportedImageFiles.clear();
        
        // get skydive types
        SkydiveTypeRepository skydiveRepository = new SkydiveTypeRepository(_context);
        for (SkydiveType skydiveType : skydiveRepository.getAllSkydiveTypes())
        {
            skydiveTypeMap.put(skydiveType.getUniqueId(), skydiveType);
        }
        // get aircrafts
        AircraftRepository aircraftRepository = new AircraftRepository(_context);
        for (Aircraft aircraft : aircraftRepository.getAllAircrafts())
        {
            aircraftMap.put(aircraft.getUniqueId(), aircraft);
        }
        // get locations
        LocationRepository locationRepository = new LocationRepository(_context);
        for (Location location : locationRepository.getAllLocations())
        {
            locationMap.put(location.getUniqueId(), location);
        }
        // get rigs
        RigRepository rigRepository = new RigRepository(_context);
        for (Rig rig : rigRepository.getAllRigs())
        {
            rigMap.put(rig.getUniqueId(), rig);
        }
    }

    private XmlSerializer initializeXml(Writer xmlWriter) throws IOException
    {
        XmlSerializer serializer = Xml.newSerializer();
        serializer.setOutput(xmlWriter);
        serializer.startDocument("UTF-8", true);
        serializer.ignorableWhitespace(NEW_LINE);
        serializer.setPrefix("", NAMESPACE);

        return serializer;
    }

    private CsvSerializer initializeCsv(Writer csvWriter) throws IOException
    {
        CsvSerializer serializer = new CsvSerializer(csvWriter);

        // write csv heder
        serializer.addRow(new String[]
        {
            _context.getString(R.string.import_export_jump_number_header),
            _context.getString(R.string.import_export_date_header),
            _context.getString(R.string.import_export_location_header),
            _context.getString(R.string.import_export_aircraft_header),
            _context.getString(R.string.import_export_gear_header),
            _context.getString(R.string.import_export_skydive_type_header),
            _context.getString(R.string.import_export_exit_alt_header),
            _context.getString(R.string.import_export_depl_alt_header),
            _context.getString(R.string.import_export_alt_unit_header),
            _context.getString(R.string.import_export_freefall_time_header),
            _context.getString(R.string.import_export_cutaway_header),
            _context.getString(R.string.import_export_notes_header),
        });

        return serializer;
    }

    private void addLogbookHistory(XmlSerializer serializer) throws IOException
    {
        // get history
    	SettingsRepository settingsRepository = new SettingsRepository(_context);
    	
        startTag(serializer, "logbook_history", 1);
        writeIntValue(serializer, "freefall_time", settingsRepository.getFreefallTime(), 2);
        writeIntValue(serializer, "cutaways", settingsRepository.getCutaways(), 2);
        endTag(serializer, "logbook_history", 1);
    }

    private void addLogEntries(XmlSerializer xmlSerializer, CsvSerializer csvSerializer) throws IOException
    {
        DatabaseCursor dbCursor = null;
        try
        {
            // repo
            LogEntryRepository repository = new LogEntryRepository(_context);

            // get log entry rigs
            Map<Integer, List<String>> logEntryRigs = new HashMap<Integer, List<String>>();
            dbCursor = repository.getAllLogEntryRigsForExportCursor();
            while (dbCursor.getCursor().moveToNext())
            {
                int logEntryId = Database.getInteger(dbCursor.getCursor(), Database.LOG_ENTRY_RIG_LOG_ENTRY);
                String rigId = Database.getString(dbCursor.getCursor(), Database.LOG_ENTRY_RIG_RIG);
                if (!logEntryRigs.containsKey(logEntryId))
                    logEntryRigs.put(logEntryId, new ArrayList<String>());
                logEntryRigs.get(logEntryId).add(rigId);
            }
            dbCursor.close();

            // get log entry signatures
            Map<Integer, Integer> logEntrySignatures = new HashMap<Integer, Integer>();
            dbCursor = repository.getAllLogEntrySignaturesForExportCursor();
            while (dbCursor.getCursor().moveToNext())
            {
                int logEntryId = Database.getInteger(dbCursor.getCursor(), Database.LOG_ENTRY_SIGNATURE_LOG_ENTRY);
                int signatureId = Database.getInteger(dbCursor.getCursor(), Database.LOG_ENTRY_SIGNATURE_SIGNATURE);
                logEntrySignatures.put(logEntryId, signatureId);
            }
            dbCursor.close();

            // get log entry images
            Map<Integer, List<LogEntryImage>> logEntryImages = new HashMap<Integer, List<LogEntryImage>>();
            dbCursor = repository.getAllLogEntryImagesForExportCursor();
            while (dbCursor.getCursor().moveToNext())
            {
                // create image
                LogEntryImage image = new LogEntryImage();
                image.setImageType(LogEntryImageType.fromStringValue(Database.getString(dbCursor.getCursor(),
                    Database.LOG_ENTRY_IMAGE_IMAGE_TYPE)));
                
            	image.setFilename(Database.getString(dbCursor.getCursor(), Database.LOG_ENTRY_IMAGE_FILENAME));
                
                image.setLogEntryId(Database.getInteger(dbCursor.getCursor(), Database.LOG_ENTRY_IMAGE_LOG_ENTRY));
                image.setMD5(Database.getString(dbCursor.getCursor(), Database.LOG_ENTRY_IMAGE_MD5));

                // add to map
                if (!logEntryImages.containsKey(image.getLogEntryId()))
                	logEntryImages.put(image.getLogEntryId(), new ArrayList<LogEntryImage>());
                logEntryImages.get(image.getLogEntryId()).add(image);
            }
            dbCursor.close();

            // get log entries
            dbCursor = repository.getAllLogEntryForExportCursor();

            // start tag
            startTag(xmlSerializer, "log_entries", 1);
            while (dbCursor.getCursor().moveToNext())
            {
                // get rig ids
                int logEntryId = Database.getInteger(dbCursor.getCursor(), Database.LOG_ENTRY_ID);
                List<String> rigIds = logEntryRigs.containsKey(logEntryId) ? logEntryRigs.get(logEntryId)
                    : new ArrayList<String>();
                // get signature id
                Integer signatureId = logEntrySignatures.containsKey(logEntryId) ? logEntrySignatures.get(logEntryId)
                    : null;
                // get images
                List<LogEntryImage> images = logEntryImages.containsKey(logEntryId) ? logEntryImages.get(logEntryId)
                    : new ArrayList<LogEntryImage>();
                // write to xml
                writeLogEntryToXml(dbCursor.getCursor(), rigIds, signatureId, images, xmlSerializer);
                // write to csv
                writeLogEntryToCsv(dbCursor.getCursor(), rigIds, csvSerializer);
            }
            endTag(xmlSerializer, "log_entries", 1);
        }
        finally
        {
            if (dbCursor != null)
            {
                dbCursor.close();
            }
        }
    }

    private void writeLogEntryToXml(Cursor cursor, List<String> rigIds, Integer signatureId,
        List<LogEntryImage> logEntryImages, XmlSerializer xmlSerializer) throws IOException
    {
        startTag(xmlSerializer, "log_entry", 2);
        writeValue(xmlSerializer, "id", Database.getString(cursor, Database.LOG_ENTRY_UNIQUE_ID), 3);
        writeIntValue(xmlSerializer, "jump_number", Database.getInteger(cursor, Database.LOG_ENTRY_JUMP_NUMBER), 3);
        writeDateValue(xmlSerializer, "date", Database.getDate(cursor, Database.LOG_ENTRY_DATE), 3);
        writeValue(xmlSerializer, "location_id", Database.getString(cursor, Database.LOG_ENTRY_LOCATION), 3);
        writeValue(xmlSerializer, "aircraft_id", Database.getString(cursor, Database.LOG_ENTRY_AIRCRAFT), 3);
        // rigs
        startTag(xmlSerializer, "rigs", 3);
        for (String rigId : rigIds)
        {
            writeValue(xmlSerializer, "rig_id", rigId, 4);
        }
        endTag(xmlSerializer, "rigs", 3);
        writeValue(xmlSerializer, "skydive_type_id", Database.getString(cursor, Database.LOG_ENTRY_SKYDIVE_TYPE), 3);
        writeIntValue(xmlSerializer, "exit_altitude", Database.getInteger(cursor, Database.LOG_ENTRY_EXIT_ALTITUDE), 3);
        writeIntValue(xmlSerializer, "deployment_altitude",
            Database.getInteger(cursor, Database.LOG_ENTRY_DEPLOYMENT_ALTITUDE), 3);
        writeValue(xmlSerializer, "altitude_unit", Database.getString(cursor, Database.LOG_ENTRY_ALTITUDE_UNIT), 3);
        writeIntValue(xmlSerializer, "freefall_time", Database.getInteger(cursor, Database.LOG_ENTRY_FREEFALL_TIME), 3);
        writeIntValue(xmlSerializer, "distance_to_target",
            Database.getInteger(cursor, Database.LOG_ENTRY_DISTANCE_TO_TARGET), 3);
        writeBooleanValue(xmlSerializer, "cutaway", Database.getBoolean(cursor, Database.LOG_ENTRY_CUTAWAY), 3);
        writeValue(xmlSerializer, "notes", Database.getString(cursor, Database.LOG_ENTRY_NOTES), 3);
        // log entry images
        startTag(xmlSerializer, "images", 3);
        for (LogEntryImage logEntryImage: logEntryImages)
        {       	
        	// export the image if a duplicate hasn't already
        	if (!exportedImageFiles.contains(logEntryImage.getFilename()))
        	{
	            // add file name to list of exported images
	            exportedImageFiles.add(logEntryImage);
        	}
            
            // write XML
            startTag(xmlSerializer, "image", 4);
            writeValue(xmlSerializer, "image_type", logEntryImage.getImageType().getStringValue(), 5);
            writeValue(xmlSerializer, "image_file", logEntryImage.getFilename(), 5);
            writeValue(xmlSerializer, "image_md5", logEntryImage.getMD5(), 5);
            endTag(xmlSerializer, "image", 4);
        }
        endTag(xmlSerializer, "images", 3);
        if (signatureId != null)
            writeIntValue(xmlSerializer, "signature_id", signatureId, 3);
        writeDateTimeValue(xmlSerializer, "last_modified_utc",
            Database.getDateTime(cursor, Database.LOG_ENTRY_LAST_MODIFIED_UTC), 3);
        if (signatureId != null)
            writeDateTimeValue(xmlSerializer, "last_signature_utc",
                Database.getDateTime(cursor, Database.LOG_ENTRY_LAST_SIGNATURE_UTC), 3);

        endTag(xmlSerializer, "log_entry", 2);
    }

    private void writeLogEntryToCsv(Cursor cursor, List<String> rigIds, CsvSerializer csvSerializer)
        throws IOException
    {
        // get location name
        String locationId = Database.getString(cursor, Database.LOG_ENTRY_LOCATION);
        String locationName = locationId == null ? "" : locationMap.get(locationId).getName();
        // get skydive type
        String skydiveTypeId = Database.getString(cursor, Database.LOG_ENTRY_SKYDIVE_TYPE);
        String skydiveTypeName = skydiveTypeId == null ? "" : skydiveTypeMap.get(skydiveTypeId).getName();
        // get aircraft name
        String aircraftId = Database.getString(cursor, Database.LOG_ENTRY_AIRCRAFT);
        String aircraftName = aircraftId == null ? "" : aircraftMap.get(aircraftId).getName();
        // get rig names
        String rigNames = getRigsString(rigIds);

        csvSerializer.addRow(new String[]
        {
            CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_JUMP_NUMBER)),
            CsvSerializer.formatDate(Database.getDate(cursor, Database.LOG_ENTRY_DATE)),
            CsvSerializer.formatString(locationName),
            CsvSerializer.formatString(aircraftName),
            CsvSerializer.formatString(rigNames),
            CsvSerializer.formatString(skydiveTypeName),
            CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_EXIT_ALTITUDE)),
            CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_DEPLOYMENT_ALTITUDE)),
            _context.getString(AltitudeUnit.valueOf(Database.getString(cursor, Database.LOG_ENTRY_ALTITUDE_UNIT))
                .getResourceId()),
            CsvSerializer.formatInteger(Database.getInteger(cursor, Database.LOG_ENTRY_FREEFALL_TIME)),
            CsvSerializer.formatBoolean(Database.getBoolean(cursor, Database.LOG_ENTRY_CUTAWAY),
                _context.getString(R.string.import_export_yes), _context.getString(R.string.import_export_no)),
            CsvSerializer.formatString(Database.getString(cursor, Database.LOG_ENTRY_NOTES))
        });
    }

    private String getRigsString(List<String> rigIds)
    {
        // set gear text
        StringBuilder builder = new StringBuilder();
        for (String rigId : rigIds)
        {
            builder.append(rigMap.get(rigId).getName());
            // if not last, append ", "
            if (rigId != rigIds.get(rigIds.size() - 1))
                builder.append(", ");
        }
        return builder.toString();
    }

    private void addSignatures(XmlSerializer serializer) throws IOException
    {
        // get signatures
        SignatureRepository repository = new SignatureRepository(_context);

        // start
        startTag(serializer, "signatures", 1);
        for (Signature signature : repository.getSignatures())
        {
            startTag(serializer, "signature", 2);
            writeIntValue(serializer, "id", signature.getId(), 3);
            writeValue(serializer, "license", signature.getLicense(), 3);
            
            File file = _fileManager.getStorageFile(signature.getFilename());
            writeImageValue(serializer, "image", _fileManager.getFileContents(file), 3);
            
            endTag(serializer, "signature", 2);
        }
        // end
        endTag(serializer, "signatures", 1);
    }

    private void addSkydiveTypes(XmlSerializer serializer) throws IOException
    {
        // start
        startTag(serializer, "skydive_types", 1);
        for (SkydiveType skydiveType : skydiveTypeMap.values())
        {
            startTag(serializer, "skydive_type", 2);
            writeValue(serializer, "id", skydiveType.getUniqueId(), 3);
            writeValue(serializer, "name", skydiveType.getName(), 3);
            writeBooleanValue(serializer, "default", skydiveType.isDefault(), 3);
            writeValue(serializer, "freefall_profile", skydiveType.getFreefallProfileType() == null ? null
                : skydiveType.getFreefallProfileType().toString(), 3);
            writeValue(serializer, "notes", skydiveType.getNotes(), 3);
            writeBooleanValue(serializer, "active", skydiveType.isActive(), 3);
            writeDateTimeValue(serializer, "last_modified_utc", skydiveType.getLastModifiedUtc(), 3);
            endTag(serializer, "skydive_type", 2);
        }
        // end
        endTag(serializer, "skydive_types", 1);
    }

    private void addLocations(XmlSerializer serializer) throws IOException
    {
        // start
        startTag(serializer, "locations", 1);
        for (Location location : locationMap.values())
        {
            startTag(serializer, "location", 2);
            writeValue(serializer, "id", location.getUniqueId(), 3);
            writeValue(serializer, "name", location.getName(), 3);
            writeBooleanValue(serializer, "home", location.isHome(), 3);
            writeValue(serializer, "notes", location.getNotes(), 3);
            writeBooleanValue(serializer, "active", location.isActive(), 3);
            writeDateTimeValue(serializer, "last_modified_utc", location.getLastModifiedUtc(), 3);
            endTag(serializer, "location", 2);
        }
        // end
        endTag(serializer, "locations", 1);
    }

    private void addAircrafts(XmlSerializer serializer) throws IOException
    {
        // start
        startTag(serializer, "aircrafts", 1);
        for (Aircraft aircraft : aircraftMap.values())
        {
            startTag(serializer, "aircraft", 2);
            writeValue(serializer, "id", aircraft.getUniqueId(), 3);
            writeValue(serializer, "name", aircraft.getName(), 3);
            writeBooleanValue(serializer, "default", aircraft.isDefault(), 3);
            writeValue(serializer, "notes", aircraft.getNotes(), 3);
            writeBooleanValue(serializer, "active", aircraft.isActive(), 3);
            writeDateTimeValue(serializer, "last_modified_utc", aircraft.getLastModifiedUtc(), 3);
            endTag(serializer, "aircraft", 2);
        }
        // end
        endTag(serializer, "aircrafts", 1);
    }

    private void addRigs(XmlSerializer serializer) throws IOException
    {
        // start
        startTag(serializer, "rigs", 1);
        for (Rig rig : rigMap.values())
        {
            startTag(serializer, "rig", 2);
            writeValue(serializer, "id", rig.getUniqueId(), 3);
            writeValue(serializer, "name", rig.getName(), 3);
            writeBooleanValue(serializer, "primary", rig.isPrimary(), 3);
            writeBooleanValue(serializer, "archived", rig.isArchived(), 3);
            writeValue(serializer, "notes", rig.getNotes(), 3);
            writeBooleanValue(serializer, "active", rig.isActive(), 3);
            writeDateTimeValue(serializer, "last_modified_utc", rig.getLastModifiedUtc(), 3);

            // components
            startTag(serializer, "components", 3);
            for (RigComponent component : rig.getComponents())
            {
                startTag(serializer, "component", 4);
                writeValue(serializer, "name", component.getName(), 5);
                writeValue(serializer, "serial_number", component.getSerialNumber(), 5);
                writeValue(serializer, "notes", component.getNotes(), 5);
                endTag(serializer, "component", 4);
            }
            endTag(serializer, "components", 3);

            // reminders
            startTag(serializer, "reminders", 3);
            for (RigReminder reminder : rig.getReminders())
            {
                startTag(serializer, "reminder", 4);
                writeValue(serializer, "name", reminder.getName(), 5);
                writeIntValue(serializer, "interval", reminder.getInterval(), 5);
                writeValue(serializer, "interval_unit", reminder.getIntervalUnit().toString(), 5);
                writeDateValue(serializer, "last_completed_date", reminder.getLastCompletedDate(), 5);
                endTag(serializer, "reminder", 4);
            }
            endTag(serializer, "reminders", 3);
            endTag(serializer, "rig", 2);
        }
        // end
        endTag(serializer, "rigs", 1);
    }

    private void startTag(XmlSerializer serializer, String tagName, int indent) throws IOException
    {
        indent(serializer, indent);
        serializer.startTag(NAMESPACE, tagName);
        serializer.ignorableWhitespace(NEW_LINE);
    }

    private void startTagWithAttribute(XmlSerializer serializer, String tagName, String attributeName,
        String attributeValue, int indent) throws IOException
    {
        indent(serializer, indent);
        serializer.startTag(NAMESPACE, tagName);
        serializer.attribute("", attributeName, attributeValue);
        serializer.ignorableWhitespace(NEW_LINE);
    }

    private void endTag(XmlSerializer serializer, String tagName, int indent) throws IOException
    {
        indent(serializer, indent);
        serializer.endTag(NAMESPACE, tagName);
        serializer.ignorableWhitespace(NEW_LINE);
    }

    private void writeIntValue(XmlSerializer serializer, String tagName, Integer value, int indent) throws IOException
    {
        writeValue(serializer, tagName, value == null ? null : String.valueOf(value), indent);
    }

    private void writeBooleanValue(XmlSerializer serializer, String tagName, Boolean value, int indent)
        throws IOException
    {
        writeValue(serializer, tagName, value == null ? null : String.valueOf(value), indent);
    }

    private void writeDateValue(XmlSerializer serializer, String tagName, Date value, int indent) throws IOException
    {
        writeValue(serializer, tagName, value == null ? null : dateFormat.format(value), indent);
    }

    private void writeDateTimeValue(XmlSerializer serializer, String tagName, Date value, int indent)
        throws IOException
    {
        writeValue(serializer, tagName, value == null ? null : dateTimeFormat.format(value), indent);
    }

    private void writeImageValue(XmlSerializer serializer, String tagName, byte[] value, int indent) throws IOException
    {
        writeValue(serializer, tagName, value == null ? null : Base64.encodeToString(value, Base64.DEFAULT), indent);
    }

    private void writeValue(XmlSerializer serializer, String tagName, String value, int indent) throws IOException
    {
        // skip if null
        if (FormatUtility.isEmptyOrNull(value))
            return;
        indent(serializer, indent);
        serializer.startTag(NAMESPACE, tagName);
        serializer.text(value);
        serializer.endTag(NAMESPACE, tagName);
        serializer.ignorableWhitespace(NEW_LINE);
    }

    private void indent(XmlSerializer serializer, int count) throws IllegalArgumentException, IllegalStateException,
        IOException
    {
        for (int i = 0; i < count; i++)
        {
            serializer.ignorableWhitespace(INDENT);
        }
    }
}
